Vue 2 存在问题
- 通过「选项」组织代码会导致,随着功能的增长,复杂组件的代码变得越来越难以维护。
- 缺少一种比较「干净」的在多个组件之间提取和复用逻辑的机制。
- 类型推断不够友好。
Vue 3 核⼼优势
- Proxy 比 Object.defineProperty 更强大,体现在:
- 可以检测属性的新增和删除
- 可以检测数组索引的变化和 length 的变化
- 支持 Map、Set、WeakMap 和 WeakSet
- 性能更比 Vue 2.0 强。更友好的
Tree-shaking⽀持(渐进式体验)更⼤的代码压缩空间,打包更科学,不再打包没用到的模块,体积更小、速度更快。 - Composition API(组合 API),没有 this,没烦恼,更灵活的逻辑复⽤能⼒。
- 三个新内置组件
- Fragment 不再需要使用 template 包裹标签,一个组件包含多个根节点
- Teleport 组件插入的位置灵活,在 DOM 中渲染一个 subtree,而不需要在一个组件中
- Suspense 异步组件
- 更好的类型推导能⼒(
TypeScript)
3.0 与 2.0 有什么差别?
- 响应式实现优化: Vue2 中响应式系统的核心 defineProperty, Vue3 中使用 Proxy 对象重写响应式系统。
- 兼容性: Vue3 打包后的代码是 ES2015+,不支持 IE 11; Vue2 不支持 IE8 及以下版本。
- 编译优化:
- diff 算法优化: 编译速度更快。vue2.x 的虚拟 DOM 是进行全量比较,vue3 新增了静态标记(PatchFlag)。
- 静态提升: vue2.x 无论元素是否参与更新,每次都会重新创新,再渲染;vue3 中对于不参与更新的元素会做静态提升 (标记和提升所有的静态根节点),只会创建一次,在渲染的时候复用即可。
- 缓存事件处理函数: 缓存事件处理函数减少了不必要的更新操作
- 源码体积优化: 包拆的更细,更小。用了 Tree-shaking, 没有用到的代码基本不会被打包进去。
- 开发语言: 切换到 ts, 包使用 Pnpm 来管理。
- 引入了很多 hooks
Vue3 的编译优化
vue2.x 和 vue 3.x 编译的部分完完全全重构了,之前 Vue2.x 采用,_c 的模式创建标签,_v 为 Vnode 节点, 而当前的 Vue 3.x 通过 _createBlock 生成 block tree
- Vue 2.x 数据更新并触发重新渲染的粒度是组件级的,单个组件内部需要遍历该组件的整个 vnode 树
- Vue.js 3.0 做到了通过编译阶段对静态模板的分析,编译生成了 Block tree。Block tree 是一个将模版基于动态节点指令切割的嵌套区块,每个区块内部的节点结构是固定的。每个区块只需要追踪自身包含的动态节点。
静态提升:静态节点都会被提升到 render 的外部,只有初始化时会被创建,再次调用 render 时不会再次创建,可以直接重用这些静态节点对应的 vnode
3. 源码体积优化
更好地支持 Tree-shaking
Vue.js 3.0 x 中移除了一些不常用的 API 例如:inline-template filter 等
在大部分情况下,我们并不需要 vue 中的所有功能,但是在之前的 vue 版本中,我们没有一个合适的办法用来除去不需要的功能,而 Vue3 中,为了满足体积更小的需求,支持 Tree-shaking,也就意味着我们可以按需求引用的内置的指令和方法。
6. 源码组织方式的变化
采用 Typescript 重写 采用 Monorepo 管理项目结构
7. composition API
Composition API 主要是提高了代码逻辑的可复用性,并且将 Reactivity 模块独立出来,这也使得 vue 3 变得更加灵活地与其他框架组合使用。
包含一个描述组件的选项(data、methods、props 等)的对象。 Options API 开发复杂组件,同一功能逻辑代码会被拆分到不同选项 Vue 3.0 新增了一组 API 一组基于函数的 API 可以更灵活的组织组件的逻辑
优点:
- 优化逻辑组织
- 优化逻辑复用。项目变得复杂的时候,免不了需要抽象出一些复用的逻辑。在 vue 2 的时候通常会使用 mixins, 使用单个 mixin 似乎问题不大,但是当我们一个组件混入大量不同的 mixins 的时候,会存在两个非常明显的问题:命名冲突和数据来源不清晰。使用组合式 api 能保证变量的整个数据来源清晰了,
静态节点提升
静态节点提升: 是「Vue3」针对 VNode 更新过程性能问题而提出的一个优化点。众所周知,在大型应用场景下,Vue2.x 的 patchVNode 过程,即 diff 过程是非常缓慢的,这是一个十分令人头疼的问题。
虽然,对于面试常问的 diff 过程在一定程度上是减少了对 DOM 的直接操作。但是,「这个减少是有一定成本的」。因为,如果是复杂应用,那么就会存在父子关系非常复杂的 VNode,而这也就是 diff 的痛点,它会不断地递归调用 patchVNode,不断堆叠而成的几毫秒,最终就会造成 VNode 更新缓慢。
「Vue3」重写了整个 Compiler 过程,提出了静态提升、靶向更新等优化点,来提高 patchVNode 过程。
什么是 patchFlag
由于,在 compile 过程的 transform 阶段会提及 AST Element 上的 patchFlag 属性。所以,在正式认识 complier 之前,我们先搞清楚一个概念,什么是 patchFlag?
patchFlag 是 complier 时的 transform 阶段解析 AST Element 打上的**「优化标识」**。并且,顾名思义 patchFlag,patch 一词表示着它会为 runtime时的 patchVNode 提供依据,从而实现靶向更新 VNode 的效果。因此,这样一来一往,也就是耳熟能详的 Vue3 巧妙结合 runtime 与 compiler 实现靶向更新和静态提升。
而在源码中 patchFlag 被定义为一个**「数字枚举类型」**,每一个枚举值对应的标识意义会是这样:

并且,值得一提的是整体上 patchFlag 的分为两大类:
- 当
patchFlag的值**「大于」** 0 时,代表所对应的元素在patchVNode时或render时是可以被优化生成或更新的。 - 当
patchFlag的值**「小于」** 0 时,代表所对应的元素在patchVNode时,是需要被full diff,即进行递归遍历VNode tree的比较更新过程。
其实,还有两类特殊的
flag:shapeFlag和slogFlag,这里我就不对此展开,有兴趣的同学可以自行去了解。
Compile 编译过程
对比 Vue2.x 编译过程
了解过「Vue2.x」源码的同学,我想应该都知道在「Vue2.x」中的 Compile 过程会是这样:
parse编译模板生成原始 AST。optimize优化原始 AST,标记 AST Element 为静态根节点或静态节点。generate根据优化后的 AST,生成可执行代码,例如_c、_l之类的。

而在「Vue3」中,整体的 Compile 过程仍然是三个阶段,但是不同于「Vue2.x」的是,第二个阶段换成了正常编译器都会存在的阶段 transform。所以,它看起来会是这样:

在源码中,它对应的伪代码会是这样:
export function baseCompile(
template: string | RootNode,
options: CompilerOptions = {}
): CodegenResult {
...
const ast = isString(template) ? baseParse(template, options) : template
...
transform(
ast,
extend({}, options, {....})
)
return generate(
ast,
extend({}, options, {
prefixIdentifiers
})
)
}
那么,我想这个时候大家可能会问为什么会是 transform?它的职责是什么?
通过简单的对比「Vue2.x」编译过程的第二阶段的 optimize,很明显,transform并不是**「无米之炊」,它仍然有着「优化」**原始 AST 的作用,而具体职责会表现在:
- 对所有 AST Element 新增
codegen属性来帮助generate更准确地生成**「最优」**的可执行代码。 - 对静态 AST Element 新增
hoists属性来实现静态节点的**「单独创建」**。 - ...
此外,transform 还标识了诸如 isBlock、helpers 等属性,来生成最优的可执行代码,这里我们就不细谈,有兴趣的同学可以自行了解。
baseParse 构建原始抽象语法树(AST)
baseParse 顾名思义起着**「解析」的作用,它的表现和「Vue2.x」的 parse 相同,都是解析模板 tempalte 生成「原始 AST」**。
假设,此时我们有一个这样的模板 template:
<div><div>hi Vue3</div><div>{{msg}}</div></div>
那么,它在经过 baseParse 处理后生成的 AST 看起来会是这样:
{
cached: 0,
children: [{…}],
codegenNode: undefined,
components: [],
directives: [],
helpers: [],
hoists: [],
imports: [],
loc: {start: {…}, end: {…}, source: "<div><div>hi Vue3</div><div>{{msg}}</div></div>"},
temps: 0,
type: 0
}
如果,了解过「Vue2.x」编译过程的同学应该对于上面这颗 AST 的大部分属性不会陌生。AST 的本质是通过用对象来描述「DSL」(特殊领域语言),例如:
children中存放的就是最外层div的后代。loc则用来描述这个 AST Element 在整个字符串(template)中的位置信息。type则是用于描述这个元素的类型(例如 5 为插值、2 为文本)等等。
并且,可以看到的是不同于「Vue2.x」的 AST,这里我们多了诸如 helpers、codegenNode、hoists 等属性。而,这些属性会在 transform 阶段进行相应地赋值,进而帮助 generate 阶段生成**「更优的」**可执行代码。
transform 优化原始抽象语法树(AST)
对于 transform 阶段,如果了解过**「编译器」**的工作流程的同学应该知道,一个完整的编译器的工作流程会是这样:
- 首先,
parse解析原始代码字符串,生成抽象语法树 AST。 - 其次,
transform转化抽象语法树,让它变成更贴近目标「DSL」的结构。 - 最后,
codegen根据转化后的抽象语法树生成目标「DSL」的可执行代码。
而在「Vue3」采用 Monorepo 的方式管理项目后,compile 对应的能力就是一个编译器。所以,transform 也是整个编译过程的重中之重。换句话说,如果没有 transform 对 AST 做诸多层面的转化,「Vue」仍然会挂在 diff 这个**「饱受诟病」**的过程。
相比之下,「Vue2.x」的编译阶段没有完整的
transform,只是optimize优化了一下 AST,可以想象在「Vue」设计之初尤大也没想到它以后会**「这么地流行」**!
那么,我们来看看 transform 函数源码中的定义:
function transform(root: RootNode, options: TransformOptions) {
const context = createTransformContext(root, options)
traverseNode(root, context)
if (options.hoistStatic) {
hoistStatic(root, context)
}
if (!options.ssr) {
createRootCodegen(root, context)
}
// finalize meta information
root.helpers = [...context.helpers]
root.components = [...context.components]
root.directives = [...context.directives]
root.imports = [...context.imports]
root.hoists = context.hoists
root.temps = context.temps
root.cached = context.cached
}
可以说,transform 函数做了什么,在它的定义中是**「一览无余」**。这里我们提一下它对静态提升其决定性作用的两件事:
- 将原始 AST 中的静态节点对应的 AST Element 赋值给根 AST 的
hoists属性。 - 获取原始 AST 需要的 helpers 对应的键名,用于
generate阶段的生成可执行代码的获取对应函数,例如createTextVNode、createStaticVNode、renderList等等。
并且,在 traverseNode 函数中会对 AST Element 应用具体的 transform 函数,大致可以分为两类:
- 静态节点
transform应用,即节点不含有插值、指令、props、动态样式的绑定等。 - 动态节点
transform应用,即节点含有插值、指令、props、动态样式的绑定等。
那么,我们就来看看对于静态节点 transform 是如何应用的?
静态节点 transform 应用
这里,对于上面我们说到的这个栗子,静态节点就是这个部分:
<div>hi Vue3</div>
而它在没有进行 transform 应用之前,它对应的 AST 会是这样:
{
children: [{
content: "hi Vue3"
loc: {start: {…}, end: {…}, source: "hi Vue3"}
type: 2
}],
codegenNode: undefined,
isSelfClosing: false,
loc: {start: {…}, end: {…}, source: "<div>hi Vue3</div>"},
ns: 0,
props: [],
tag: "div",
tagType: 0,
type: 1
}
可以看出,此时它的 codegenNode 是 undefined。而在源码中各类 transform函数被定义为 plugin,它会根据 baseParse 生成的 AST **「递归应用」**对应的 plugin。然后,创建对应 AST Element 的 codegen 对象。
所以,此时我们会命中 transformElement 和 transformText 两个 plugin的逻辑。
「transformText」
transformText 顾名思义,它和**「文本」**相关。很显然,此时的 AST Element 所属的类型就是 Text。那么,我们先来看一下 transformText 函数对应的伪代码:
export const transformText: NodeTransform = (node, context) => {
if (
node.type === NodeTypes.ROOT ||
node.type === NodeTypes.ELEMENT ||
node.type === NodeTypes.FOR ||
node.type === NodeTypes.IF_BRANCH
) {
return () => {
const children = node.children
let currentContainer: CompoundExpressionNode | undefined = undefined
let hasText = false
for (let i = 0; i < children.length; i++) { // {1}
const child = children[i]
if (isText(child)) {
hasText = true
...
}
}
if (
!hasText ||
(children.length === 1 &&
(node.type === NodeTypes.ROOT ||
(node.type === NodeTypes.ELEMENT &&
node.tagType === ElementTypes.ELEMENT)))
) { // {2}
return
}
...
}
}
}
可以看到,这里我们会命中 「{2}」 的逻辑,即如果对于**「节点含有单一文本」** transformText 并不需要进行额外的处理,即该节点仍然在这里仍然保留和「Vue2.x」版本一样的处理方式。
而 transfromText 真正发挥作用的场景是当模板中存在这样的情况:
<div>ab {a} {b}</div>
此时 transformText 需要将两者放在一个**「单独的」** AST Element 下,在源码中它被称为「Compound Expression」,即**「组合的表达式」。这种组合的目的是为了 patchVNode 这类 VNode 时做到「更好地定位和实现 DOM 的更新」**。反之,如果是一个文本节点和插值动态节点的话,在 patchVNode 阶段同样的操作需要进行两次,例如对于同一个 DOM 节点操作两次。
「transformElement」
transformElement 是一个所有 AST Element 都会被执行的一个 plugin,它的核心是为 AST Element 生成最基础的 codegen 属性。例如标识出对应 patchFlag,从而为生成 VNode 提供依据,例如 dynamicChildren。
而对于静态节点,同样是起到一个初始化它的 codegenNode 属性的作用。并且,从上面介绍的 patchFlag 的类型,我们可以知道它的 patchFlag 为默认值 0。所以,它的 codegenNode 属性值看起来会是这样:
{
children: {
content: "hi Vue3"
loc: {start: {…}, end: {…}, source: "hi Vue3"}
type: 2
},
directives: undefined,
disableTracking: false,
dynamicProps: undefined,
isBlock: false,
loc: {start: {…}, end: {…}, source: "<div>hi Vue3</div>"},
patchFlag: undefined,
props: undefined,
tag: ""div"",
type: 13
}
generate 生成可执行代码
generate 是 compile 阶段的最后一步,它的作用是将 transform 转换后的 AST 生成对应的**「可执行代码」**,从而在之后 Runtime 的 Render 阶段时,就可以通过可执行代码生成对应的 VNode Tree,然后最终映射为真实的 DOM Tree 在页面上。
同样地,这一阶段在「Vue2.x」也是由 generate 函数完成,它会生成是诸如 _l、_c 之类的函数,这本质上是对 _createElement 函数的封装。而相比较「Vue2.x」版本的 generate,「Vue3」改变了很多,其 generate 函数对应的伪代码会是这样:
export function generate(
ast: RootNode,
options: CodegenOptions & {
onContextCreated?: (context: CodegenContext) => void
} = {}
): CodegenResult {
const context = createCodegenContext(ast, options)
if (options.onContextCreated) options.onContextCreated(context)
const {
mode,
push,
prefixIdentifiers,
indent,
deindent,
newline,
scopeId,
ssr
} = context
...
genFunctionPreamble(ast, context)
...
if (!ssr) {
...
push(`function render(_ctx, _cache${optimizeSources}) {`)
}
....
return {
ast,
code: context.code,
// SourceMapGenerator does have toJSON() method but it's not in the types
map: context.map ? (context.map as any).toJSON() : undefined
}
}
所以,接下来,我们就来**「一睹」**带有静态节点对应的 AST 生成的可执行代码的过程会是怎样。
CodegenContext 代码生成上下文
从上面 generate 函数的伪代码可以看到,在函数的开始调用了 createCodegenContext 为当前 AST 生成了一个 context。在整个 generate 函数的执行过程**「都依托」**于一个 CodegenContext 「生成代码上下文」(对象)的能力,它是通过 createCodegenContext 函数生成。而 CodegenContext 的接口定义会是这样:
interface CodegenContext
extends Omit {
source: string
code: string
line: number
column: number
offset: number
indentLevel: number
pure: boolean
map?: SourceMapGenerator
helper(key: symbol): string
push(code: string, node?: CodegenNode): void
indent(): void
deindent(withoutNewLine?: boolean): void
newline(): void
}
可以看到 CodegenContext 对象中有诸如 push、indent、newline 之类的方法。而它们的作用是在根据 AST 来生成代码时用来**「实现换行」、「添加代码」、「缩进」**等功能。从而,最终形成一个个可执行代码,即我们所认知的 render 函数,并且,它会作为 CodegenContext 的 code 属性的值返回。
下面,我们就来看下静态节点的可执行代码生成的核心,它被称为 Preamble 前导。
genFunctionPreamble 生成前准备
整个静态提升的可执行代码生成就是在 genFunctionPreamble 函数部分完成的。并且,大家仔细**「斟酌」一番静态提升的字眼,静态二字我们可以不看,但是「提升二字」,直抒本意地表达出它(静态节点)被「提高了」**。
为什么说是提高了?因为在源码中的体现,确实是被提高了。在前面的 generate 函数,我们可以看到 genFunctionPreamble 是先于 render 函数加入context.code 中,所以,在 Runtime 时的 Render 阶段,它会先于 render 函数执行。
geneFunctionPreamble 函数(伪代码):
function genFunctionPreamble(ast: RootNode, context: CodegenContext) {
const {
ssr,
prefixIdentifiers,
push,
newline,
runtimeModuleName,
runtimeGlobalName
} = context
...
const aliasHelper = (s: symbol) => `${helperNameMap[s]}: _${helperNameMap[s]}`
if (ast.helpers.length > 0) {
...
if (ast.hoists.length) {
const staticHelpers = [
CREATE_VNODE,
CREATE_COMMENT,
CREATE_TEXT,
CREATE_STATIC
]
.filter(helper => ast.helpers.includes(helper))
.map(aliasHelper)
.join(', ')
push(`const { ${staticHelpers} } = _Vue\n`)
}
}
...
genHoists(ast.hoists, context)
newline()
push(`return `)
}
可以看到,这里会对前面我们在 transform 函数提及的 hoists 属性的长度进行判断。显然,对于前面说的这个栗子,它的 ast.hoists.length 长度是大于 0 的。所以,这里就会根据 hoists 中的 AST 生成对应的可执行代码。因此,到这里,生成的可执行代码会是这样:
const _Vue = Vue
const { createVNode: _createVNode } = _Vue
// 静态提升部分
const _hoisted_1 = _createVNode("div", null, "hi Vue3", -1 /* HOISTED */)
// render 函数会在这下面
小结
静态节点提升在整个 compile 编译阶段体现,从最初的 baseCompile 到 transform 转化原始 AST、再到 generate 的优先 render 函数处理生成可执行代码,最后交给 Runtime 时的 Render 执行,这种设计可以说是非常精妙!所以,这样一来,就完成了我们经常看到在一些文章提及的「Vue3」对于静态节点在整个生命周期中它只会执行**「一次创建」**的源码实现,这在一定程度上降低了性能上的开销。
两者 diff 算法有什么区别? 静态标记?
Vue.js 2.x 的数据更新并触发重新渲染的粒度是组件级的,虽然 Vue 能保证触发更新的组件最小化,但在单个组件内部依然需要遍历该组件的整个 vnode 树。如果一个组件的 template 很大,但是动态部分比较少。因为这段代码中只有一个动态节点,所以这里有很多 diff 和遍历其实都是不需要的,这就会导致 vnode 的性能跟模版大小正相关,跟动态节点的数量无关,当一些组件的整个模版内只有少量动态节点时,这些遍历都是性能的浪费。
而对于上述例子,理想状态只需要 diff 这个绑定 message 动态节点的 p 标签即可。 Vue.js 3.0 做到了,它通过编译阶段对静态模板的分析,编译生成了 Block tree。Block tree 是一个将模版基于动态节点指令切割的嵌套区块,每个区块内部的节点结构是固定的,而且每个区块只需要以一个 Array 来追踪自身包含的动态节点。借助 Block tree,Vue.js 将 vnode 更新性能由与模版整体大小相关提升为与动态内容的数量相关,这是一个非常大的性能突破。
除此之外,Vue.js 3.0 在编译阶段还包含了对 Slot 的编译优化、事件侦听函数的缓存优化,并且在运行时重写了 diff 算法
总结优点
Vue3 通过 Proxy 响应式+组件内部 vdom+静态标记,把任务颗粒度控制的足够细致,所以也不太需要 time-slice 了,就可能不太需要 react 的 fiber 这种时间切片的概念了。